Atbrīvojiet ātrāku, efektīvāku kodu. Apgūstiet regulāro izteiksmju optimizācijas paņēmienus, no atkāpšanās un alkatīgās/slinkās saskaņošanas līdz specifiskai dzinēja pielāgošanai.
Regulāro izteiksmju optimizācija: padziļināts ieskats Regex veiktspējas pielāgošanā
Regulārās izteiksmes jeb regex ir neaizstājams rīks mūsdienu programmētāja instrumentu kopumā. No lietotāja ievades validēšanas un žurnālfailu parsēšanas līdz sarežģītām meklēšanas un aizvietošanas operācijām un datu ekstrakcijai – to spēks un daudzpusība ir nenoliedzama. Tomēr šim spēkam ir slēptas izmaksas. Slikti uzrakstīts regex var kļūt par klusu veiktspējas slepkavu, radot ievērojamu latentumu, izraisot CPU slodzes lēcienus un, sliktākajos gadījumos, pilnībā apturot jūsu lietojumprogrammu. Tieši šeit regulāro izteiksmju optimizācija kļūst ne tikai par 'vēlamu', bet par kritiski svarīgu prasmi, lai veidotu robustu un mērogojamu programmatūru.
Šis visaptverošais ceļvedis jūs aizvedīs dziļā ceļojumā regex veiktspējas pasaulē. Mēs izpētīsim, kāpēc šķietami vienkāršs raksts var būt katastrofāli lēns, sapratīsim regex dzinēju iekšējo darbību un apbruņosim jūs ar spēcīgu principu un paņēmienu kopumu, lai rakstītu regulārās izteiksmes, kas ir ne tikai pareizas, bet arī zibensātras.
Izpratne par 'kāpēc': sliktas Regex izmaksas
Pirms mēs pievēršamies optimizācijas paņēmieniem, ir svarīgi saprast problēmu, ko mēs cenšamies atrisināt. Visnopietnākā veiktspējas problēma, kas saistīta ar regulārajām izteiksmēm, ir pazīstama kā Katastrofāla atkāpšanās (Catastrophic Backtracking), stāvoklis, kas var novest pie Regulāro izteiksmju pakalpojumatteices (ReDoS) ievainojamības.
Kas ir katastrofāla atkāpšanās?
Katastrofāla atkāpšanās notiek, kad regex dzinējam nepieciešams ārkārtīgi ilgs laiks, lai atrastu atbilstību (vai noteiktu, ka atbilstība nav iespējama). Tas notiek ar noteikta veida rakstiem pret noteikta veida ievades virknēm. Dzinējs iesprūst reibinošā permutāciju labirintā, izmēģinot katru iespējamo ceļu, lai apmierinātu rakstu. Soļu skaits var pieaugt eksponenciāli līdz ar ievades virknes garumu, novedot pie tā, kas šķiet kā lietojumprogrammas sasalšana.
Apsveriet šo klasisko ievainojamā regex piemēru: ^(a+)+$
Šis raksts šķiet pietiekami vienkāršs: tas meklē virkni, kas sastāv no viena vai vairākiem 'a'. Tas lieliski darbojas ar virknēm kā "a", "aa" un "aaaaa". Problēma rodas, kad mēs to testējam pret virkni, kas gandrīz atbilst, bet galu galā neizdodas, piemēram, "aaaaaaaaaaaaaaaaaaaaaaaaaaab".
Lūk, kāpēc tas ir tik lēns:
- Ārējais
(...)+un iekšējaisa+abi ir alkatīgi kvantifikatori. - Iekšējais
a+vispirms atbilst visiem 27 'a' burtiem. - Ārējais
(...)+ir apmierināts ar šo vienu atbilstību. - Dzinējs pēc tam mēģina saskaņot virknes beigu enkuru
$. Tas neizdodas, jo tur ir 'b'. - Tagad dzinējam ir jāatkāpjas. Ārējā grupa atdod vienu rakstzīmi, tāpēc iekšējais
a+tagad atbilst 26 'a' burtiem, un ārējās grupas otrā iterācija mēģina saskaņot pēdējo 'a'. Tas arī neizdodas pie 'b'. - Dzinējs tagad izmēģinās katru iespējamo veidu, kā sadalīt 'a' virkni starp iekšējo
a+un ārējo(...)+. N 'a' burtu virknei ir 2N-1 veidi, kā to sadalīt. Sarežģītība ir eksponenciāla, un apstrādes laiks strauji pieaug.
Šis viens, šķietami nekaitīgais regex var bloķēt CPU kodolu uz sekundēm, minūtēm vai pat ilgāk, efektīvi liedzot pakalpojumu citiem procesiem vai lietotājiem.
Lietas būtība: Regex dzinējs
Lai optimizētu regex, jums ir jāsaprot, kā dzinējs apstrādā jūsu rakstu. Pastāv divi galvenie regex dzinēju veidi, un to iekšējā darbība nosaka veiktspējas īpašības.
DFA (Determinētu galīgo automātu) dzinēji
DFA dzinēji ir regex pasaules ātruma dēmoni. Tie apstrādā ievades virkni vienā piegājienā no kreisās uz labo pusi, rakstzīmi pēc rakstzīmes. Jebkurā brīdī DFA dzinējs precīzi zina, kāds būs nākamais stāvoklis, pamatojoties uz pašreizējo rakstzīmi. Tas nozīmē, ka tam nekad nav jāatkāpjas. Apstrādes laiks ir lineārs un tieši proporcionāls ievades virknes garumam. Rīku piemēri, kas izmanto DFA bāzētus dzinējus, ir tradicionālie Unix rīki, piemēram, grep un awk.
Plusi: Ārkārtīgi ātra un paredzama veiktspēja. Imūni pret katastrofālu atkāpšanos.
Mīnusi: Ierobežots funkciju kopums. Tie neatbalsta tādas uzlabotas funkcijas kā atpakaļatsauces, apskates mehānismus vai saglabāšanas grupas, kas balstās uz spēju atkāpties.
NFA (Nedeterminētu galīgo automātu) dzinēji
NFA dzinēji ir visizplatītākais veids, ko izmanto mūsdienu programmēšanas valodās, piemēram, Python, JavaScript, Java, C# (.NET), Ruby, PHP un Perl. Tie ir "raksta vadīti", kas nozīmē, ka dzinējs seko rakstam, virzoties pa virkni. Kad tas sasniedz neskaidrības punktu (piemēram, alternatīvu | vai kvantifikatoru *, +), tas izmēģinās vienu ceļu. Ja šis ceļš galu galā neizdodas, tas atkāpjas uz pēdējo lēmuma punktu un izmēģina nākamo pieejamo ceļu.
Šī atkāpšanās spēja padara NFA dzinējus tik jaudīgus un funkcijām bagātus, ļaujot izmantot sarežģītus rakstus ar apskates mehānismiem un atpakaļatsaucēm. Tomēr tas ir arī viņu Ahileja papēdis, jo tas ir mehānisms, kas padara iespējamu katastrofālu atkāpšanos.
Turpmākajā šī ceļveža daļā mūsu optimizācijas paņēmieni koncentrēsies uz NFA dzinēja savaldīšanu, jo tieši šeit izstrādātāji visbiežāk saskaras ar veiktspējas problēmām.
NFA dzinēju optimizācijas pamatprincipi
Tagad pievērsīsimies praktiskiem, pielietojamiem paņēmieniem, kurus varat izmantot, lai rakstītu augstas veiktspējas regulārās izteiksmes.
1. Esiet specifiski: precizitātes spēks
Visizplatītākais veiktspējas antipaterns ir pārāk vispārīgu aizstājējzīmju, piemēram, .*, izmantošana. Punkts . atbilst (gandrīz) jebkurai rakstzīmei, un zvaigznīte * nozīmē "nulle vai vairāk reižu". Kombinējot, tie liek dzinējam alkatīgi paņemt visu atlikušo virkni un pēc tam atkāpties pa vienai rakstzīmei, lai redzētu, vai pārējā raksta daļa var atbilst. Tas ir neticami neefektīvi.
Slikts piemērs (HTML virsraksta parsēšana):
<title>.*</title>
Lielā HTML dokumentā .* vispirms saskaņos visu līdz faila beigām. Pēc tam tas atkāpsies, rakstzīmi pēc rakstzīmes, līdz atradīs pēdējo </title>. Tas ir daudz nevajadzīga darba.
Labs piemērs (izmantojot nolieguma rakstzīmju klasi):
<title>[^<]*</title>
Šī versija ir daudz efektīvāka. Nolieguma rakstzīmju klase [^<]* nozīmē "saskaņot jebkuru rakstzīmi, kas nav '<', nulle vai vairāk reižu". Dzinējs virzās uz priekšu, patērējot rakstzīmes, līdz sasniedz pirmo '<'. Tam nekad nav jāatkāpjas. Šī ir tieša, nepārprotama instrukcija, kas nodrošina milzīgu veiktspējas pieaugumu.
2. Pārvaldiet alkatību pret slinkumu: jautājuma zīmes spēks
Kvantifikatori regex pēc noklusējuma ir alkatīgi. Tas nozīmē, ka tie saskaņo pēc iespējas vairāk teksta, vienlaikus ļaujot vispārējam rakstam atbilst.
- Alkatīgie:
*,+,?,{n,m}
Jūs varat padarīt jebkuru kvantifikatoru slinku, pievienojot tam jautājuma zīmi. Slinks kvantifikators saskaņo pēc iespēmas mazāk teksta.
- Slinkie:
*?,+?,??,{n,m}?
Piemērs: treknraksta tagu saskaņošana
Ievades virkne: <b>First</b> and <b>Second</b>
- Alkatīgais raksts:
<b>.*</b>
Tas saskaņos:<b>First</b> and <b>Second</b>..*alkatīgi paņēma visu līdz pēdējam</b>. - Slinkais raksts:
<b>.*?</b>
Tas pirmajā mēģinājumā saskaņos<b>First</b>un, ja meklēsiet vēlreiz,<b>Second</b>..*?saskaņoja minimālo rakstzīmju skaitu, kas nepieciešams, lai pārējā raksta daļa (</b>) varētu atbilst.
Lai gan slinkums var atrisināt noteiktas saskaņošanas problēmas, tas nav universāls risinājums veiktspējai. Katrā slinkās saskaņošanas solī dzinējam ir jāpārbauda, vai nākamā raksta daļa atbilst. Ļoti specifisks raksts (piemēram, nolieguma rakstzīmju klase no iepriekšējā punkta) bieži ir ātrāks par slinku.
Veiktspējas secība (no ātrākā uz lēnāko):
- Specifiska/nolieguma rakstzīmju klase:
<b>[^<]*</b> - Slinkais kvantifikators:
<b>.*?</b> - Alkatīgais kvantifikators ar lielu atkāpšanos:
<b>.*</b>
3. Izvairieties no katastrofālas atkāpšanās: ligzdoto kvantifikatoru savaldīšana
Kā redzējām sākotnējā piemērā, tiešais katastrofālās atkāpšanās cēlonis ir raksts, kurā kvantificēta grupa satur citu kvantifikatoru, kas var saskaņot to pašu tekstu. Dzinējs saskaras ar neskaidru situāciju ar vairākiem veidiem, kā sadalīt ievades virkni.
Problemātiskie raksti:
(a+)+(a*)*(a|aa)+(a|b)*, kur ievades virkne satur daudz 'a' un 'b'.
Risinājums ir padarīt rakstu nepārprotamu. Jums jānodrošina, ka dzinējam ir tikai viens veids, kā saskaņot doto virkni.
4. Izmantojiet atomārās grupas un īpašnieciskos kvantifikatorus
Šis ir viens no spēcīgākajiem paņēmieniem, kā izslēgt atkāpšanos no jūsu izteiksmēm. Atomārās grupas un īpašnieciskie kvantifikatori saka dzinējam: "Kad esat saskaņojis šo raksta daļu, nekad neatdodiet nevienu no rakstzīmēm. Neatkāpieties šajā izteiksmē."
Īpašnieciskie kvantifikatori
Īpašniecisko kvantifikatoru izveido, pievienojot + aiz parasta kvantifikatora (piem., *+, ++, ?+, {n,m}+). Tos atbalsta tādi dzinēji kā Java, PCRE (PHP, R) un Ruby.
Piemērs: skaitļa saskaņošana, kam seko 'a'
Ievades virkne: 12345
- Parastais Regex:
\d+a\d+saskaņo "12345". Pēc tam dzinējs mēģina saskaņot 'a' un neizdodas. Tas atkāpjas, tāpēc\d+tagad saskaņo "1234", un tas mēģina saskaņot 'a' pret '5'. Tas turpinās, līdz\d+ir atdevis visas savas rakstzīmes. Tas ir daudz darba, lai ciestu neveiksmi. - Īpašnieciskais Regex:
\d++a\d++īpašnieciski saskaņo "12345". Pēc tam dzinējs mēģina saskaņot 'a' un neizdodas. Tā kā kvantifikators bija īpašniecisks, dzinējam ir aizliegts atkāpties\d++daļā. Tas nekavējoties cieš neveiksmi. To sauc par 'ātro neveiksmi' un tas ir ārkārtīgi efektīvi.
Atomārās grupas
Atomārajām grupām ir sintakse (?>...) un tās tiek atbalstītas plašāk nekā īpašnieciskie kvantifikatori (piemēram, .NET, Python jaunākajā `regex` modulī). Tās darbojas tāpat kā īpašnieciskie kvantifikatori, bet attiecas uz visu grupu.
Regex (?>\d+)a ir funkcionāli līdzvērtīgs \d++a. Jūs varat izmantot atomārās grupas, lai atrisinātu sākotnējo katastrofālās atkāpšanās problēmu:
Sākotnējā problēma: (a+)+
Atomārais risinājums: ((?>a+))+
Tagad, kad iekšējā grupa (?>a+) saskaņo 'a' virkni, tā nekad neatdos tos ārējai grupai, lai mēģinātu vēlreiz. Tas novērš neskaidrību un novērš eksponenciālo atkāpšanos.
5. Alternatīvu secībai ir nozīme
Kad NFA dzinējs sastopas ar alternatīvu (izmantojot | simbolu), tas izmēģina alternatīvas no kreisās uz labo pusi. Tas nozīmē, ka jums vispirms jāievieto visticamākā alternatīva.
Piemērs: komandas parsēšana
Iedomājieties, ka jūs parsējat komandas un zināt, ka `GET` komanda parādās 80% gadījumu, `SET` 15% gadījumu un `DELETE` 5% gadījumu.
Mazāk efektīvi: ^(DELETE|SET|GET)
80% jūsu ievades gadījumu dzinējs vispirms mēģinās saskaņot `DELETE`, neizdosies, atkāpsies, mēģinās saskaņot `SET`, neizdosies, atkāpsies un beidzot gūs panākumus ar `GET`.
Efektīvāk: ^(GET|SET|DELETE)
Tagad 80% gadījumu dzinējs iegūst atbilstību jau pirmajā mēģinājumā. Šī nelielā izmaiņa var būtiski ietekmēt veiktspēju, apstrādājot miljoniem rindu.
6. Izmantojiet nesaglabājošās grupas, ja saglabāšana nav nepieciešama
Iekavas (...) regex veic divas lietas: tās grupē apakšrakstu un saglabā tekstu, kas atbilda šim apakšrakstam. Šis saglabātais teksts tiek glabāts atmiņā vēlākai lietošanai (piemēram, atpakaļatsaucēs kā `\1` vai ekstrakcijai no izsaucošā koda). Šai glabāšanai ir neliela, bet izmērāma papildu slodze.
Ja jums ir nepieciešama tikai grupēšanas uzvedība, bet nav nepieciešams saglabāt tekstu, izmantojiet nesaglabājošo grupu: (?:...).
Saglabājošā: (https?|ftp)://([^/]+)
Šī grupa saglabā "http" un domēna nosaukumu atsevišķi.
Nesaglabājošā: (?:https?|ftp)://([^/]+)
Šeit mēs joprojām grupējam `https?|ftp`, lai `://` tiktu piemērots pareizi, bet mēs nesaglabājam saskaņoto protokolu. Tas ir nedaudz efektīvāk, ja jūs interesē tikai domēna nosaukuma (kas ir 1. grupā) ekstrakcija.
Papildu paņēmieni un dzinējspecifiski padomi
Apskates mehānismi (Lookarounds): spēcīgi, bet jālieto uzmanīgi
Apskates mehānismi (lookahead (?=...), (?!...) un lookbehind (?<=...), (?) ir nulles platuma apgalvojumi. Tie pārbauda nosacījumu, faktiski nepatērējot nekādas rakstzīmes. Tas var būt ļoti efektīvi konteksta validācijai.
Piemērs: paroles validācija
Regex, lai validētu paroli, kurai jāsatur cipars:
^(?=.*\d).{8,}$
Tas ir ļoti efektīvi. Apskates mehānisms (lookahead) (?=.*\d) skenē uz priekšu, lai nodrošinātu, ka cipars pastāv, un pēc tam kursors atgriežas sākumā. Raksta galvenajai daļai, .{8,}, tad vienkārši jāsaskaņo 8 vai vairāk rakstzīmes. Tas bieži ir labāk nekā sarežģītāks, viena ceļa raksts.
Iepriekšēja aprēķināšana un kompilācija
Lielākā daļa programmēšanas valodu piedāvā veidu, kā "kompilēt" regulāro izteiksmi. Tas nozīmē, ka dzinējs vienreiz parsē raksta virkni un izveido optimizētu iekšējo attēlojumu. Ja jūs izmantojat vienu un to pašu regex vairākas reizes (piemēram, ciklā), jums tas vienmēr jākompilē vienu reizi ārpus cikla.
Python piemērs:
import re
# Kompilē regex vienu reizi
log_pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
for line in log_file:
# Izmanto kompilēto objektu
match = log_pattern.search(line)
if match:
print(match.group(1))
Ja to nedara, dzinējs katrā iterācijā ir spiests no jauna parsēt raksta virkni, kas ir ievērojams CPU ciklu izšķērdējums.
Praktiski rīki Regex profilēšanai un atkļūdošanai
Teorija ir lieliska, bet redzēt nozīmē noticēt. Mūsdienu tiešsaistes regex testētāji ir nenovērtējami rīki veiktspējas izpratnei.
Tīmekļa vietnes, piemēram, regex101.com, nodrošina "Regex atkļūdotāja" vai "soļu skaidrojuma" funkciju. Jūs varat ielīmēt savu regex un testa virkni, un tas sniegs jums soli pa solim izsekojamu informāciju par to, kā NFA dzinējs apstrādā virkni. Tas skaidri parāda katru saskaņošanas mēģinājumu, neveiksmi un atkāpšanos. Šis ir labākais veids, kā vizualizēt, kāpēc jūsu regex ir lēns, un pārbaudīt apspriesto optimizāciju ietekmi.
Praktisks kontrolsaraksts Regex optimizācijai
Pirms sarežģīta regex ieviešanas, pārbaudiet to pēc šī mentālā kontrolsaraksta:
- Specifiskums: Vai esmu izmantojis slinku
.*?vai alkatīgu.*, kur specifiskāka nolieguma rakstzīmju klase, piemēram,[^"\r\n]*, būtu ātrāka un drošāka? - Atkāpšanās: Vai man ir ligzdoti kvantifikatori, piemēram,
(a+)+? Vai pastāv neskaidrība, kas varētu novest pie katastrofālas atkāpšanās noteiktos ievades datos? - Īpašnieciskums: Vai es varu izmantot atomāro grupu
(?>...)vai īpašniecisko kvantifikatoru*+, lai novērstu atkāpšanos apakšrakstā, par kuru zinu, ka to nevajadzētu pārvērtēt? - Alternatīvas: Vai manos
(a|b|c)alternatīvās visbiežāk sastopamā alternatīva ir norādīta pirmā? - Saglabāšana: Vai man ir nepieciešamas visas manas saglabājošās grupas? Vai dažas var pārveidot par nesaglabājošām grupām
(?:...), lai samazinātu papildu slodzi? - Kompilācija: Ja es izmantoju šo regex ciklā, vai es to iepriekš kompilēju?
Gadījuma izpēte: žurnālfailu parsētāja optimizācija
Saliksim visu kopā. Iedomāsimies, ka mēs parsējam standarta tīmekļa servera žurnāla rindu.
Žurnāla rinda: 127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
Pirms (lēns Regex):
^(\S+) (\S+) (\S+) \[(.*)\] "(.*)" (\d+) (\d+)$
Šis raksts ir funkcionāls, bet neefektīvs. (.*) datumam un pieprasījuma virknei ievērojami atkāpsies, īpaši, ja ir bojātas žurnāla rindas.
Pēc (optimizēts Regex):
^(\S+) (\S+) (\S+) \[[^\]]+\] "(?:GET|POST|HEAD) ([^ "]+) HTTP/[\d.]+" (\d{3}) (\d+)$
Uzlabojumu skaidrojums:
\[(.*)\]kļuva par\[[^\]]+\]. Mēs aizstājām vispārīgo, atkāpjošos.*ar ļoti specifisku nolieguma rakstzīmju klasi, kas saskaņo visu, izņemot noslēdzošo iekavu. Atkāpšanās nav nepieciešama."(.*)"kļuva par"(?:GET|POST|HEAD) ([^ "]+) HTTP/[\d.]+". Šis ir milzīgs uzlabojums.- Mēs esam skaidri norādījuši HTTP metodes, kuras sagaidām, izmantojot nesaglabājošo grupu.
- Mēs saskaņojam URL ceļu ar
[^ "]+(viena vai vairākas rakstzīmes, kas nav atstarpe vai pēdiņa), nevis ar vispārīgu aizstājējzīmi. - Mēs norādām HTTP protokola formātu.
(\d+)statusa kodam tika sašaurināts uz(\d{3}), jo HTTP statusa kodi vienmēr ir trīs ciparu gari.
'Pēc' versija ir ne tikai dramatiski ātrāka un drošāka pret ReDoS uzbrukumiem, bet tā ir arī robustāka, jo tā stingrāk validē žurnāla rindas formātu.
Noslēgums
Regulārās izteiksmes ir abpusgriezīgs zobens. Lietojot tās uzmanīgi un ar zināšanām, tās ir elegants risinājums sarežģītām teksta apstrādes problēmām. Lietojot neuzmanīgi, tās var kļūt par veiktspējas murgu. Galvenā atziņa ir apzināties NFA dzinēja atkāpšanās mehānismu un rakstīt rakstus, kas virza dzinēju pa vienu, nepārprotamu ceļu, cik bieži vien iespējams.
Esot specifiskiem, izprotot alkatības un slinkuma kompromisus, novēršot neskaidrības ar atomārajām grupām un izmantojot pareizos rīkus savu rakstu testēšanai, jūs varat pārveidot savas regulārās izteiksmes no potenciālas saistības par spēcīgu un efektīvu resursu savā kodā. Sāciet profilēt savu regex jau šodien un atbrīvojiet ātrāku, uzticamāku lietojumprogrammu.